


package rsi.mesoscopic.util;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

/**
 * 
 * @author godinho
 *
 * @param <T>
 */
public class DiskSwap <T extends Serializable> {
	
	private Connection mSqliteConn;
	
	private File mDBFile;
	
	private static final int MAX_CACHED_APPENDS = 100;
	
	private static final String TABLE = "CREATE TABLE cache (" +
			"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
			"object BLOB" +
			")";
	
	private static final String INSERT = "INSERT INTO cache (object) VALUES (?)";
	
	private static final String SELECT = "SELECT object FROM cache LIMIT ? OFFSET ?";
	
	private PreparedStatement mBatchInsertStmt;
	
	private PreparedStatement mSelectStmt;
	
	private int mBatchSize = 0;
	
	private boolean mHasPendingFlush = false;
	
	private int mPointer = 0;
	
	/**
	 * Create a new disk swap using directory as object repository.
	 * 
	 * This constructor returns an error on write permission errors.
	 * 
	 * @param directory Directory where files will be stored.
	 * @throws IOException
	 */
	public DiskSwap(File directory) throws IOException {
		
		mDBFile = new File(directory, this.toString() +".db");
		
		try {
			openSQLite(mDBFile);
		} catch (ClassNotFoundException e) {
			throw new IOException(e.getMessage());
		} catch (SQLException e) {
			throw new IOException(e.getMessage());
		}
	}
	
	/**
	 * Add a new object on this class.
	 * 
	 * @param object that will be stored on disk
	 * @return true when store can be performed with success
	 * @throws IOException
	 */
	public boolean append(T o) throws IOException {

		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		ObjectOutputStream oout = new ObjectOutputStream(baos);
		oout.writeObject(o);
		oout.close();
		
		try {
			mBatchInsertStmt.setBytes(1, baos.toByteArray());
			mBatchInsertStmt.addBatch();
			
			mBatchSize++;
			
			if(mBatchSize >= MAX_CACHED_APPENDS) {
				flush();
			}
			
		} catch (SQLException e) {
			throw new IOException(e.getMessage());
		}
		
		mHasPendingFlush = true;
	
		return true;
	}
	
	/**
	 * Get next element
	 * 
	 * @param c Number of elements to be retrieved
	 * @return element
	 * @throws IOException
	 */
	public List<T> get(int c) throws IOException {
		
		//Perform flush
		checkForPendingFlush();		
		
		List<T> objects = new ArrayList<T>();
		
		try {
			mSelectStmt.setInt(1, c);
			mSelectStmt.setInt(2, mPointer);
			
			ResultSet rs = mSelectStmt.executeQuery();
			
			while(rs.next()) {
				byte b[] = rs.getBytes(1);
				ByteArrayInputStream bis = new ByteArrayInputStream(b);
				ObjectInputStream ois = new ObjectInputStream(bis);
				@SuppressWarnings("unchecked")
				T obj = (T) ois.readObject();
				objects.add(obj);
			}
			
		} catch (SQLException e) {
			e.printStackTrace();
			throw new IOException(e.getMessage());
		} catch (ClassNotFoundException e) {
			throw new IOException(e.getMessage());
		}

		mPointer += objects.size();
		
		return objects;
	}
	
	/**
	 * Move index to position i, the next operation will be performed on this
	 * position.
	 * 
	 * @param i
	 * @throws IOException
	 */
	public void seek(int i) throws IOException {
		mPointer = i;
	}
	
	/**
	 * Removes all resources used by this disk swap.
	 */
	public void release() {
		if(mDBFile.exists()) {
			mDBFile.delete();
		}
	}
	
	private void flush() throws SQLException {
		mBatchInsertStmt.executeBatch();
		mBatchInsertStmt.clearBatch();
		mHasPendingFlush = false;
		mBatchSize = 0;
	}
	
	private void checkForPendingFlush() throws IOException {
		if(mHasPendingFlush) {
			try {
				flush();
			} catch (SQLException e) {
				throw new IOException(e.getMessage());
			}
		}
	}
	
	private void openSQLite(File f) throws ClassNotFoundException, SQLException {
		Class.forName("org.sqlite.JDBC");
		mSqliteConn = DriverManager.getConnection("jdbc:sqlite:"+f.toString());
		
		//Create table
		Statement createTableStmt = mSqliteConn.createStatement();
		createTableStmt.execute(TABLE);
		
		mBatchInsertStmt = mSqliteConn.prepareStatement(INSERT);
		mSelectStmt = mSqliteConn.prepareStatement(SELECT);
	}
}
